﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace PhpDocBlockGenerator.Model.Php
{
    public class PhpProperty : IPhpClassElements
    {
        /// <summary>
        /// the whole file
        /// </summary>
        private List<String> _fileArr;

        /// <summary>
        /// the line of the property (in the file)
        /// </summary>
        public int BeginningLine { get; private set; }
        
        /// <summary>
        /// the name of the property
        /// </summary>
        private string _propertyName = String.Empty;
        public String PropertyName
        {
            get { return _propertyName; }
            set { _propertyName = value; }
        }

        /// <summary>
        /// the datatype of the property
        /// </summary>
        private PhpDataType _propertyType = new PhpDataType();
        public PhpDataType PropertyType
        {
            get { return _propertyType; }
            set { _propertyType = value; }
        }

        /// <summary>
        /// the docblock created by the program
        /// </summary>
        public PhpPropertyDocBlock GeneratedDocBlock { get; set; }

        /// <summary>
        /// the merged docblock of userdoc and generated doc
        /// </summary>
        public PhpPropertyDocBlock MergedDocBlock { get; set; }

        /// <summary>
        /// the docblock the user has defined
        /// </summary>
        public PhpPropertyDocBlock UserDocBlock { get; set; }

        /// <summary>
        /// the class constructor (to evaluate some property inits)
        /// </summary>
        public PhpMethod Constructor { get; set; }

        /// <summary>
        /// the indent for the docblock
        /// </summary>
        private String _indent = String.Empty;

        /// <summary>
        /// the options container
        /// </summary>
        private Options _options;

        /// <summary>
        /// ctor
        /// </summary>
        /// <param name="fileArr">the whole file in a list (of lines)</param>
        /// <param name="beginningLine">the line where the property was found</param>
        /// <param name="propertyName">the name of the property</param>
        /// <param name="ctor"></param>
        /// <param name="options"></param>
        public PhpProperty(List<String> fileArr, int beginningLine, String propertyName, PhpMethod ctor, Options options)
        {
            _fileArr = fileArr;
            BeginningLine = beginningLine;
            PropertyName = propertyName;
            Constructor = ctor;
            _options = options;

            _GenerateDocBlock();
            _ParsePropertyDocBlock();
        }

        /// <summary>
        /// generates the docblock for the actual property
        /// </summary>
        private void _GenerateDocBlock()
        {
            GeneratedDocBlock = new PhpPropertyDocBlock(_options);
            GeneratedDocBlock.ShortDescription.Description = "<Description of " + PropertyName + ">";

            _GetIndent();
            _SearchForPropertyType();
        }

        /// <summary>
        /// searches for an initialization of the property
        /// </summary>
        private void _SearchForPropertyType()
        {
            PropertyType.Type = PhpDataTypes.Mixed;
            // at first check if the property is initialized directly
            if (PhpRegex.RegSearchAssignment.IsMatch(_fileArr[BeginningLine]))
            {
                // write all lines of the assignment into a list
                var assignmentList = new List<String>();
                int line = BeginningLine;

                // now search for the closing semicolon and add all lines to the list
                do
                {
                    assignmentList.Add(_fileArr[line]);
                } while (!PhpRegex.RegSearchOpeningCurlyBraceOrSemicolon.IsMatch(_fileArr[line++]));

                // check the assignment list for datatypes
                var datatypeRegexes = new Dictionary<PhpDataTypes, Regex>
                    {
                        { PhpDataTypes.Float, PhpRegex.RegSearchFloat },
                        { PhpDataTypes.Int, PhpRegex.RegSearchInt },
                        { PhpDataTypes.String, PhpRegex.RegSearchString },
                        { PhpDataTypes.Array, PhpRegex.RegSearchArray },
                        { PhpDataTypes.Boolean, PhpRegex.RegSearchBoolean }
                    };

                PhpDataTypes type = PhpDataTypes.Mixed;
                foreach (String aLine in assignmentList)
                {
                    KeyValuePair<PhpDataTypes, Regex> match = datatypeRegexes.FirstOrDefault(x => x.Value.IsMatch(aLine));
                    type = match.Key;
                    //foreach (KeyValuePair<PhpDataTypes, Regex> datatypeRegex in datatypeRegexes.Where(datatypeRegex => datatypeRegex.Value.IsMatch(aLine)))
                    //{
                    //    type = datatypeRegex.Key;
                    //    break;
                    //}
                    if (type != PhpDataTypes.Mixed)
                        break;
                }

                PropertyType.Type = type;
            }
            // else check the constructor for some initializations
            else if (Constructor != null)
            {
                // get the body of the constructor
                String ctorBody = Constructor.MethodBody;
                // get the property name without the php specific variable prefix
                String propNameWithoutDollar = PropertyName.Substring(1);

                // create regex to find the init
                Regex regSearchForInitialization = 
                    new Regex(@"\$this-\>" + propNameWithoutDollar + @"[\s\t]*\=[^\;]*");
                Match initMatch = regSearchForInitialization.Match(ctorBody);

                // split by =
                String[] initParts = initMatch.Value.Split(new[] {"="}, StringSplitOptions.None);
                if (initParts.Length >= 2)
                {
                    // get last part (maybe it is $var = $blubb = 1)
                    string assignment = initParts[initParts.Length-1].Trim();

                    // check for a variable
                    Match regVariableMatch = PhpRegex.RegSearchVarName.Match(assignment);
                    if (regVariableMatch.Success)
                    {
                        // it is a variable -> check if it is a constructor param
                        if (Constructor.MergedDocBlock == null)
                            Constructor.CompareAndMerge();
                        DocBlockPropertyParam param =
                            Constructor.MergedDocBlock.Params
                                       .FirstOrDefault(x => x.VariableName.Equals(assignment));
                        if (param != null)
                        {
                            PropertyType = param.VariableDataType.Clone() as PhpDataType;
                        }
                    }
                    // assignment isnt a variable, check for a specific datatype
                    else
                    {
                        // all regexes into a dictionary (to get it via linq)
                        var datatypeRegexes = new Dictionary<PhpDataTypes, Regex>
                            {
                                {PhpDataTypes.Misc, PhpRegex.RegSearchNew},
                                {PhpDataTypes.Float, PhpRegex.RegSearchFloat},
                                {PhpDataTypes.Int, PhpRegex.RegSearchInt},
                                {PhpDataTypes.String, PhpRegex.RegSearchString},
                                {PhpDataTypes.Array, PhpRegex.RegSearchArray},
                                {PhpDataTypes.Boolean, PhpRegex.RegSearchBoolean}
                            };

                        PhpDataTypes type = datatypeRegexes.FirstOrDefault(x => x.Value.IsMatch(assignment)).Key;
                        if (type == PhpDataTypes.Misc)
                        {
                            Match regNewDataTypeMatch = PhpRegex.RegSearchNew.Match(assignment);
                            PropertyType.SetTypeFromString(regNewDataTypeMatch.Value);
                        }
                        PropertyType.Type = type;
                    }
                }
            }
            GeneratedDocBlock.Var.VariableDataType = PropertyType;
        }

        /// <summary>
        /// calculates the indent for the doc block (takes it from the line which contains
        /// the function-keyword)
        /// </summary>
        private void _GetIndent()
        {
            if (-1 != BeginningLine)
            {
                Match indentMatch = PhpRegex.RegSearchIndent.Match(_fileArr[BeginningLine]);

                if (indentMatch.Success)
                    _indent = indentMatch.Value;
            }
        }

        /// <summary>
        /// parses the doc block
        /// </summary>
        private void _ParsePropertyDocBlock()
        {
            int begin, end;
            UserDocBlock = new PhpPropertyDocBlock(_options) {IsUserBlock = true};
            
            // find begin and end of a possible docblock
            StaticMethods.ParseDocBlockPos(_fileArr, BeginningLine, out begin, out end);

            //if doc block exists
            if (begin != -1 && end != -1)
            {
                //extract it from file lines array, parse it and save position
                List<String> docBlockLines = _fileArr.GetRange(begin, end - begin + 1);
                UserDocBlock.Parse(docBlockLines);
                UserDocBlock.DocBlockBegin = begin;
                UserDocBlock.DocBlockEnd = end;
            }
        }

        /// <summary>
        /// compares the generated and the userdocblock and creates a merged one
        /// </summary>
        public void CompareAndMerge()
        {
            MergedDocBlock = UserDocBlock == null ? GeneratedDocBlock : GeneratedDocBlock.Compare(UserDocBlock);
        }

        /// <summary>
        /// writes the changed lines back to the list of file lines
        /// </summary>
        /// <param name="allFileLines">complete file as list of lines</param>
        /// <param name="writingPos">position to begin writing in the complete list of file lines</param>
        /// <param name="writingOffset">offset of earlier written lines to regard for</param>
        public void WriteToListOfFileLines(List<String> allFileLines, int writingPos, ref int writingOffset)
        {
            //TODO: need own exception type here
            //if no merged doc block exists, throw exception
            if (null == MergedDocBlock)
                throw new Exception();

            //calc new position of all lines (due to earlier writing operations in the list of lines)
            int newPos = BeginningLine + writingPos;

            //get doc block as list of lines
            List<String> docBlockAsList = MergedDocBlock.ToList(_indent);

            //calc number of lines of old doc block (if exists)
            int numberOfLinesOldBlock = 0;
            if (null != UserDocBlock &&
                -1 != UserDocBlock.DocBlockBegin && -1 != UserDocBlock.DocBlockEnd)
            {
                numberOfLinesOldBlock =
                    UserDocBlock.DocBlockEnd - UserDocBlock.DocBlockBegin + 1;

                //delete space-lines between docblock and method
                int spaceLines = BeginningLine - UserDocBlock.DocBlockEnd - 1;
                if (spaceLines > 0)
                {
                    allFileLines.RemoveRange(newPos - spaceLines, spaceLines);
                    newPos -= spaceLines;
                    writingOffset -= spaceLines;
                }
            }

            //calc difference between old and new doc block
            int diffNumberOfLines = docBlockAsList.Count - numberOfLinesOldBlock;
            int newPosOldDocBlock = newPos - numberOfLinesOldBlock;

            //remove old block and add new one
            allFileLines.RemoveRange(newPosOldDocBlock, numberOfLinesOldBlock);
            allFileLines.InsertRange(newPosOldDocBlock, docBlockAsList);

            writingOffset += diffNumberOfLines;
        }
    }
}
